{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##### Copyright 2020 The OpenFermion Developers"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#@title Licensed under the Apache License, Version 2.0 (the \"License\");\n",
    "# you may not use this file except in compliance with the License.\n",
    "# You may obtain a copy of the License at\n",
    "#\n",
    "# https://www.apache.org/licenses/LICENSE-2.0\n",
    "#\n",
    "# Unless required by applicable law or agreed to in writing, software\n",
    "# distributed under the License is distributed on an \"AS IS\" BASIS,\n",
    "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
    "# See the License for the specific language governing permissions and\n",
    "# limitations under the License."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Introduction to FQE"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<table class=\"tfo-notebook-buttons\" align=\"left\">\n",
    "  <td>\n",
    "    <a target=\"_blank\" href=\"https://quantumai.google/openfermion/fqe/guide/introduction\"><img src=\"https://quantumai.google/site-assets/images/buttons/quantumai_logo_1x.png\" />View on QuantumAI</a>\n",
    "  </td>\n",
    "  <td>\n",
    "    <a target=\"_blank\" href=\"https://colab.research.google.com/github/quantumlib/OpenFermion/blob/master/docs/fqe/guide/introduction.ipynb\"><img src=\"https://quantumai.google/site-assets/images/buttons/colab_logo_1x.png\" />Run in Google Colab</a>\n",
    "  </td>\n",
    "  <td>\n",
    "    <a target=\"_blank\" href=\"https://github.com/quantumlib/OpenFermion/blob/master/docs/fqe/guide/introduction.ipynb\"><img src=\"https://quantumai.google/site-assets/images/buttons/github_logo_1x.png\" />View source on GitHub</a>\n",
    "  </td>\n",
    "  <td>\n",
    "    <a href=\"https://storage.googleapis.com/tensorflow_docs/OpenFermion/docs/fqe/guide/introduction.ipynb\"><img src=\"https://quantumai.google/site-assets/images/buttons/download_icon_1x.png\" />Download notebook</a>\n",
    "  </td>\n",
    "</table>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "OpenFermion-FQE is an emulator for quantum computing specialized for simulations of Fermionic many-body problems, where FQE stands for 'Fermionic quantum emulator.' By focusing on Fermionic physics, OpenFermion-FQE realizes far more efficient emulation of quantum computing than generic quantum emulators such as Cirq, both in computation and memory costs; the speed-up and improved memory footprint originate from the use of the spin and number symmetries as well as highly optimized special algorithms.\n",
    "\n",
    "The examples of the problems that can be simulated by OpenFermion-FQE include those in molecular electronic structure, condensed matter physics, and nuclear physics.\n",
    "\n",
    "The initial version of OpenFermion-FQE has been developed in collaboration between [QSimulate](https://qsimulate.com)  and [Google Quantum AI](https://quantumai.google/). The source code is found in the GitHub repository (https://github.com/quantumlib/OpenFermion-FQE).\n",
    "\n",
    "This tutorial will describe the data structures and conventions of the library."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    import fqe\n",
    "except ImportError:\n",
    "    !pip install fqe --quiet"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import fqe\n",
    "import numpy as np"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The FQE Wavefunction"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `Wavefunction` is an interface to the objects that hold the actual wavefunction data.  As mentioned, the wavefunction is partitioned into sectors with fixed particle and $Sz$ quantum number.  This partitioning information is the necessary information for initializing a `Wavefunction` object.  \n",
    "\n",
    "As an example, we consider initializing a wavefunction with four spatial orbitals, four electrons, and different $Sz$ expectation values.  The `Wavefunction` object takes a list of these sectors `[[n_electrons, sz, n_orbits]]`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "wfn = fqe.Wavefunction([[4, 4, 4], [4, 2, 4], [4, 0, 4], [4, -2, 4], [4, -4, 4]])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This command initializes a wavefunction with the following block structure:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "![Image of wf sectors](./wf_init_sectors.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Each sector corresponds to a set of bit strings \n",
    "\n",
    "$$\n",
    "\\vert I \\rangle = \\vert I_{\\alpha}I_{\\beta}\\rangle\n",
    "$$ \n",
    "\n",
    "that encode a fixed particle number and fixed $Sz$ expectation. The coefficients associated with the bitstrings in these sectors are formed into matrices.  This helps with efficient vectorized computations. The row-index of the array corresponds to the $\\alpha$ spin-orbital number occupation index and the column-index corresponds to the $\\beta$-strings.  The `Wavefunction` object provides tools to access sectors or perform basic mathematical operations on this *vector*.  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Methods to initialize wavefunctions"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "FQE wavefunctions can be initialized by calling the constructor directly."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "wfn_fqe = fqe.Wavefunction([[2, -2, 4]], broken=None)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "When wavefunctions are first created, they are initialized to empty values. We can see this by printing out the wavefunction. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "wfn_fqe.print_wfn()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To set the values of a wavefunction, we can use the `set_wfn` method with a provided `strategy`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "wfn_fqe.set_wfn(strategy=\"hartree-fock\")\n",
    "wfn_fqe.print_wfn()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Users can access the wavefunction through the `get_sector` method.  This returns the entire matrix of data representing the specified sector of the wavefunction.  For example, we can grab the sector corresponding to $n = 2$ and $sz = -2$ by doing the following."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "interesting_states = wfn_fqe.get_coeff((2, -2))\n",
    "print(interesting_states)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Other than the `Wavefunction` constructor, several utility methods are available to initialize wavefunctions. The function `fqe.get_wavefunction` builds a wavefunction with definite particle number and spin."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "wfn_fqe = fqe.get_wavefunction(4, -2, 10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The function `fqe.get_wavefunction_multiple` constructs multiple wavefunctions with different particle number, spin, and orbital number."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "wfn_fqe1, wfn_fqe2 = fqe.get_wavefunction_multiple([[4, 0, 10], [5, -5, 10]])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "There are also functions like `fqe.get_number_conserving_wavefunction` and  `fqe.get_spin_conserving_wavefunction` to get number or spin conserving wavefunctions, respectively."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Get a spin conserving wavefunction.\n",
    "spin_conserving_wfn = fqe.get_spin_conserving_wavefunction(2, 4)\n",
    "\n",
    "# Get a number conserving wavefunction.\n",
    "number_conserving_wfn = fqe.get_number_conserving_wavefunction(2, 4)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Conversions between FQE and Cirq wavefunction representations"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Wavefunctions on $n$ qubits in Cirq are represented by Numpy arrays with $2^n$ amplitudes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "nqubits = 4\n",
    "\n",
    "cirq_wfn = np.random.rand(2**nqubits) + 1.0j * np.random.rand(2**nqubits)\n",
    "cirq_wfn /= np.linalg.norm(cirq_wfn)\n",
    "\n",
    "print(\"Cirq wavefunction:\")\n",
    "print(*cirq_wfn, sep=\"\\n\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To convert from this representation to the FQE representation, the function `fqe.from_cirq` can be used."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fqe_wfn = fqe.from_cirq(cirq_wfn, thresh=0.0001)\n",
    "fqe_wfn.print_wfn()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> *Note*: The `thresh` argument is the value below which amplitudes are considered zero."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can convert back to the Cirq representation using `fqe._to_cirq`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cirq_wfn_from_fqe = fqe.to_cirq(fqe_wfn)\n",
    "\n",
    "print(\"Cirq wavefunction from FQE:\")\n",
    "print(*cirq_wfn_from_fqe, sep=\"\\n\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "assert np.allclose(cirq_wfn_from_fqe, cirq_wfn)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "An important thing to note in these conversions is the ordering of the $\\alpha$ and $\\beta$ strings in the converted wavefunctions.  The FQE uses the OpenFermion convention of interleaved $\\alpha$ and $\\beta$ orbitals.  Thus when converting to Cirq we first convert each bitstring into an OpenFermion operator and then call normal ordering."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Printing and saving wavefunctions"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Printing is currently available as alpha beta strings followed by the coefficient as well as orbital occupation representation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "print('String formatting')\n",
    "fqe_wfn.print_wfn(fmt='str')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "print('Occupation formatting')\n",
    "fqe_wfn.print_wfn(fmt='occ')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Wavefunctions can also be saved to disk using the `save` method which takes a filename and optional path."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Action on Wavefunctions: Fermionic algebra operations and their unitaries on the state"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "FermionOperators can be directly passed in to create a new wavefunction based on application of the operators.  The FermionOperators are parsed according to the interleaved $\\alpha$ $\\beta$ indexing of the spin-orbitals.  This means that odd index FermionOperators correspond to $\\beta$-spin orbitals and even are $\\alpha$-spin orbitals.  \n",
    "\n",
    "Sharp Edge:\n",
    "\n",
    "The user must be careful to not break the symmetry of the wavefunction.  If a request to apply an operator to a state takes the wavefunction outside of the specified symmetry sector the FQE will not execute the command.  Effectively, the FQE requires the user to have more knowledge of what type of operations their `Wavefunction` object can support."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from openfermion import FermionOperator, hermitian_conjugated\n",
    "\n",
    "ops = FermionOperator('2^ 0', 1.2)\n",
    "\n",
    "new_wfn = fqe_wfn.apply(ops + hermitian_conjugated(ops))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Unitary operations"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Any simulator backend must be able to perform unitary evolution on a state.  The FQE accomplishes this by implementing code for evolving a state via the action of a unitary generated by fermionic generators.  Given a fermion operator $g$, the unitary \n",
    "\n",
    "$$\n",
    "e^{-i (g + g^{\\dagger})}\n",
    "$$\n",
    "\n",
    "can be applied to the state.  It can be shown that this evolution operator can be rewritten as \n",
    "\n",
    "$$\n",
    "e^{-i(g + g^{\\dagger})\\epsilon } = \\cos\\left(\\epsilon\\right) \\mathbb{I}_{s}(gg^{\\dagger}) +  \\cos\\left(\\epsilon\\right) \\mathbb{I}_{s}(g^{\\dagger}g) - i\\sin\\left(\\epsilon\\right) \\left(g + g^{\\dagger}\\right) \\left[\\mathbb{I}_{s}(gg^{\\dagger}) + \\mathbb{I}_{s}(g^{\\dagger}g)\\right] + \\mathbb{I}_{!s}\n",
    "$$\n",
    "\n",
    "The $\\mathbb{I}_{!s}$ is for setting the coefficients of the unitary that are not in the subspace $\\mathcal{H}_{s} \\subset \\mathcal{H}$ where $gg^{\\dagger}$ is 0.   \n",
    "\n",
    "The user can specify a fermionic monomial in OpenFermion and use the `time_evolve` method of the `Wavefunction` object to call the evolution. All the rules for preserving symmetries must be maintained as before."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "i, j, theta = 0, 1, np.pi / 3\n",
    "op = (FermionOperator(((2 * i, 1), (2 * j, 0)), coefficient=-1j * theta) + \n",
    "      FermionOperator(((2 * j, 1), (2 * i, 0)), coefficient=1j * theta))\n",
    "\n",
    "new_wfn = fqe_wfn.time_evolve(1.0, op)\n",
    "\n",
    "new_wfn.print_wfn()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In other tutorials we will do a deeper dive into supported time-evolution operations.  To serve a full functioning time-evolution platform the FQE also implements arbitrary time-evolution of full Hamiltonian operators consisting of sums of non-commuting terms.  The Taylor and Chebyshev expansion methods are used to do the exact time evolution.  "
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
